// Shaders should NOT be modified
/* eslint-disable no-useless-escape */

import { WebGlProgramHelper } from '@app/static/webgl-program';
import { mat4, vec4 } from 'gl-matrix';
import { postProcessingShaderCode } from '../../lib/post-processing-shader-code';

enum ProgramType {
  OCCLUSION,
  SMAA_BLEND,
  SMAA_WEIGHTS,
  GODRAYS,
  BLUR,
  BLOOM,
}

export type OcclusionProgram = {
  program: WebGLProgram;
  type: ProgramType;
  attr: {
    position: number;
  };
  uniform: {
    uPMatrix: WebGLUniformLocation;
    uMvMatrix: WebGLUniformLocation;
    uCamMatrix: WebGLUniformLocation;
  };
  attrSetup: (vertPosBuf: WebGLBuffer, stride?: number) => void;
  attrOff: () => void;
  uniformSetup: (mvMatrix: mat4, pMatrix: mat4, camMatrix: mat4) => void;
};

export class PostProcessingManager {
  public curBuffer: WebGLFramebuffer;
  public secBuffer: WebGLFramebuffer;
  private gl_: WebGL2RenderingContext;
  private isReady_ = false;
  public programs = {
    occlusion: {
      program: <WebGLProgram>null,
      type: ProgramType.OCCLUSION,
      attr: {
        position: <number>null,
      },
      uniform: {
        uPMatrix: <WebGLUniformLocation>null,
        uMvMatrix: <WebGLUniformLocation>null,
        uCamMatrix: <WebGLUniformLocation>null,
      },
      attrSetup: null,
      attrOff: null,
      uniformSetup: null,
    },
  };

  private frameBufferInfos = {
    one: {
      frameBuffer: <WebGLFramebuffer>null,
      texture: <WebGLTexture>null,
      depthBuffer: <WebGLRenderbuffer>null,
    },
    two: {
      frameBuffer: <WebGLFramebuffer>null,
      texture: <WebGLTexture>null,
      depthBuffer: <WebGLRenderbuffer>null,
    },
  };

  private shaderCode = postProcessingShaderCode;
  curFbi: {
    texture: WebGLTexture;
    frameBuffer: WebGLFramebuffer;
    buffers: {
      position: WebGLBuffer;
      texCoord: WebGLBuffer;
    };
  };

  secFbi: {
    texture: WebGLTexture;
    frameBuffer: WebGLFramebuffer;
    buffers: {
      position: WebGLBuffer;
      texCoord: WebGLBuffer;
    };
  };

  public init(gl: WebGL2RenderingContext): void {
    if (this.isReady_) {
      return;
    }
    this.gl_ = gl;

    /*
     * this.programs.hdr = this.createProgram(this.shaderCode.hdr.vert, this.shaderCode.hdr.frag);
     * this.programs.hdr = this.setupHdr(gl, this.programs.hdr);
     */

    /*
     * this.programs.fxaa = this.createProgram(this.shaderCode.fxaa.vert, this.shaderCode.fxaa.frag);
     * this.programs.fxaa = this.setupFxaa(gl, this.programs.fxaa);
     */

    /*
     * this.programs.smaaEdges = this.createProgram(this.shaderCode.smaaEdges.vert, this.shaderCode.smaaEdges.frag);
     * this.programs.smaaEdges = this.setupSmaaEdges(gl, this.programs.smaaEdges);
     */

    /*
     * this.programs.smaaWeights = this.createProgram(this.shaderCode.smaaWeights.vert, this.shaderCode.smaaWeights.frag);
     * this.programs.smaaWeights = this.setupSmaaWeights(gl, this.programs.smaaWeights);
     */

    /*
     * this.programs.smaaBlend = this.createProgram(this.shaderCode.smaaBlend.vert, this.shaderCode.smaaBlend.frag);
     * this.programs.smaaBlend = this.setupSmaaBlend(gl, this.programs.smaaBlend);
     */

    this.programs.occlusion.program = new WebGlProgramHelper(this.gl_, this.shaderCode.occlusion.vert, this.shaderCode.occlusion.frag).program;
    this.gl_.useProgram(this.programs.occlusion.program);
    this.setupOcclusion();

    /*
     * this.programs.gaussian = this.createProgram(this.shaderCode.gaussian.vert, this.shaderCode.gaussian.frag);
     * this.programs.gaussian = this.setupGaussian(gl, this.programs.gaussian);
     */

    /*
     * this.frameBufferInfos.one = this.createFrameBufferInfo(gl.drawingBufferWidth, gl.drawingBufferHeight);
     * this.frameBufferInfos.two = this.createFrameBufferInfo(gl.drawingBufferWidth, gl.drawingBufferHeight);
     */

    // this.curBuffer = this.frameBufferInfos.one.frameBuffer;
    this.curBuffer = null;

    this.isReady_ = true;
  }

  public setupOcclusion() {
    const gl = this.gl_;

    this.programs.occlusion.attr = {
      position: gl.getAttribLocation(this.programs.occlusion.program, 'a_position'),
    };
    this.programs.occlusion.uniform = {
      uPMatrix: gl.getUniformLocation(this.programs.occlusion.program, 'uPMatrix'),
      uCamMatrix: gl.getUniformLocation(this.programs.occlusion.program, 'uCamMatrix'),
      uMvMatrix: gl.getUniformLocation(this.programs.occlusion.program, 'uMvMatrix'),
    };
    this.programs.occlusion.attrSetup = (combinedBuf: WebGLBuffer, stride = Float32Array.BYTES_PER_ELEMENT * 8): void => {
      gl.bindBuffer(gl.ARRAY_BUFFER, combinedBuf);
      gl.enableVertexAttribArray(this.programs.occlusion.attr.position);
      gl.vertexAttribPointer(this.programs.occlusion.attr.position, 3, gl.FLOAT, false, stride, 0);
    };
    this.programs.occlusion.attrOff = (): void => {
      gl.disableVertexAttribArray(this.programs.occlusion.attr.position);
    };
    this.programs.occlusion.uniformSetup = (mvMatrix: vec4, pMatrix: vec4, camMatrix: vec4): void => {
      gl.uniformMatrix4fv(this.programs.occlusion.uniform.uMvMatrix, false, mvMatrix);
      gl.uniformMatrix4fv(this.programs.occlusion.uniform.uPMatrix, false, pMatrix);
      gl.uniformMatrix4fv(this.programs.occlusion.uniform.uCamMatrix, false, camMatrix);
    };
  }

  public createFrameBufferInfo(width: number, height: number) {
    const gl = this.gl_;
    const frameBufferInfo = {
      width,
      height,
      buffers: {
        position: <WebGLBuffer>null,
        texCoord: <WebGLBuffer>null,
      },
      texture: <WebGLTexture>null,
      frameBuffer: <WebGLFramebuffer>null,
      renderBuffer: <WebGLRenderbuffer>null,
    };

    frameBufferInfo.buffers.position = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, frameBufferInfo.buffers.position);
    const x1 = 0;
    const x2 = 0 + width;
    const y1 = 0;
    const y2 = 0 + height;

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]), gl.STATIC_DRAW);

    // provide texture coordinates for the rectangle.
    frameBufferInfo.buffers.texCoord = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, frameBufferInfo.buffers.texCoord);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]), gl.STATIC_DRAW);

    frameBufferInfo.texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, frameBufferInfo.texture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); // makes clearing work
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

    frameBufferInfo.frameBuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, frameBufferInfo.frameBuffer);

    frameBufferInfo.renderBuffer = gl.createRenderbuffer(); // create RB to store the depth buffer
    gl.bindRenderbuffer(gl.RENDERBUFFER, frameBufferInfo.renderBuffer);
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);

    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, frameBufferInfo.texture, 0);
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, frameBufferInfo.renderBuffer);

    return frameBufferInfo;
  }

  public switchFrameBuffer() {
    this.curBuffer = this.curBuffer == this.frameBufferInfos.one.frameBuffer ? this.frameBufferInfos.two.frameBuffer : this.frameBufferInfos.one.frameBuffer;
    this.secBuffer = this.curBuffer == this.frameBufferInfos.one.frameBuffer ? this.frameBufferInfos.two.frameBuffer : this.frameBufferInfos.one.frameBuffer;
  }

  public getFrameBufferInfo(curBuffer: WebGLFramebuffer): any {
    if (curBuffer == this.frameBufferInfos.one.frameBuffer || curBuffer == this.frameBufferInfos.two.frameBuffer) {
      const frameBufferInfo = curBuffer == this.frameBufferInfos.one.frameBuffer ? this.frameBufferInfos.one : this.frameBufferInfos.two;


      return frameBufferInfo;
    }
    // Parse godrays buffer -- TEMPORARY SOLUTION

    return {
      buffers: {
        position: (<any>curBuffer).positionBuffer,
        texCoord: (<any>curBuffer).texcoordBuffer,
      },
      texture: (<any>curBuffer).godraysTexture,
    };

  }

  public clearAll() {
    const gl = this.gl_;

    gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBufferInfos.one.frameBuffer);
    gl.clearColor(0.0, 0.0, 0.0, 0.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBufferInfos.two.frameBuffer);
    /*
     * DEBUG:
     * gl.clearColor(0.0, 0.0, 0.0, 0.0);
     */
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  }

  public doPostProcessing(program: OcclusionProgram | any, programType: ProgramType, tgtBuffer: WebGLBuffer | null) {
    const gl = this.gl_;
    const curBuffer = this.curBuffer;
    const secBuffer = this.secBuffer;

    gl.useProgram(program);
    // Make sure pPM texture doesn't prevent the rest of the scene from rendering
    gl.depthMask(false);
    this.curFbi = this.getFrameBufferInfo(curBuffer);
    // Turn on the position attribute
    gl.enableVertexAttribArray(program.attr.position);
    // Bind the position buffer.
    gl.bindBuffer(gl.ARRAY_BUFFER, this.curFbi.buffers.position);
    // Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
    gl.vertexAttribPointer(program.attr.position, 2, gl.FLOAT, false, 0, 0);
    // Turn on the texcoord attribute
    gl.enableVertexAttribArray(program.attr.texCoord);
    // bind the texcoord buffer.
    gl.bindBuffer(gl.ARRAY_BUFFER, this.curFbi.buffers.texCoord);
    // Tell the texcoord attribute how to get data out of texcoordBuffer (ARRAY_BUFFER)
    gl.vertexAttribPointer(program.attr.texCoord, 2, gl.FLOAT, false, 0, 0);
    // Load the original image
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, this.curFbi.texture);
    gl.uniform1i(program.uniform.canvas, 0);

    switch (programType) {
      case ProgramType.SMAA_WEIGHTS:
        gl.activeTexture(gl.TEXTURE1);
        gl.bindTexture(gl.TEXTURE_2D, program.textures.area);
        gl.uniform1i(program.uniform.area, 1);
        gl.activeTexture(gl.TEXTURE2);
        gl.bindTexture(gl.TEXTURE_2D, program.textures.search);
        gl.uniform1i(program.uniform.search, 2);
        break;
      case ProgramType.SMAA_BLEND:
        this.secFbi = this.getFrameBufferInfo(secBuffer);
        gl.activeTexture(gl.TEXTURE1);
        gl.bindTexture(gl.TEXTURE_2D, this.secFbi.texture);
        gl.uniform1i(program.uniform.color, 1);
        break;
      default:
        break;
    }

    // set the resolution
    gl.uniform2f(program.uniform.resolution, gl.canvas.width, gl.canvas.height);
    /*
     * this.setupAdditionalUniforms(program);
     * Draw the new version to the screen
     */
    gl.bindFramebuffer(gl.FRAMEBUFFER, tgtBuffer);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
    // Future writing needs to have a depth test
    gl.depthMask(true);
  }
}

/*
 * DEBUG: Future Use
 * pPM.setupHdr = (gl, program) => {
 *   program.attr = {
 *     position: gl.getAttribLocation(program, 'a_position'),
 *     texCoord: gl.getAttribLocation(program, 'a_texCoord'),
 *   };
 *   program.uniform = {
 *     resolution: gl.getUniformLocation(program, 'u_resolution'),
 *     canvas: gl.getUniformLocation(program, 'u_canvas'),
 *   };
 *   program.uniformSetup = () => null;
 *   return program;
 * };
 */

/*
 * DEBUG: Future Use
 * pPM.setupFxaa = (gl, program) => {
 *   program.attr = {
 *     position: gl.getAttribLocation(program, 'a_position'),
 *     texCoord: gl.getAttribLocation(program, 'a_texCoord'),
 *   };
 *   program.uniform = {
 *     resolution: gl.getUniformLocation(program, 'u_resolution'),
 *     canvas: gl.getUniformLocation(program, 'u_canvas'),
 *   };
 *   program.uniformSetup = () => null;
 *   return program;
 * };
 */

/*
 * DEBUG: Future Use
 * pPM.setupSmaaBlend = (gl, program) => {
 *   program.attr = {
 *     position: gl.getAttribLocation(program, 'a_position'),
 *     texCoord: gl.getAttribLocation(program, 'a_texCoord'),
 *   };
 *   program.uniform = {
 *     resolution: gl.getUniformLocation(program, 'u_resolution'),
 *     canvas: gl.getUniformLocation(program, 'u_canvas'),
 *     color: gl.getUniformLocation(program, 'u_color'),
 *   };
 *   program.uniformSetup = () => null;
 *   return program;
 * };
 */

/*
 * DEBUG: Future Use
 * pPM.setupSmaaEdges = (gl, program) => {
 *   program.attr = {
 *     position: gl.getAttribLocation(program, 'a_position'),
 *     texCoord: gl.getAttribLocation(program, 'a_texCoord'),
 *   };
 *   program.uniform = {
 *     resolution: gl.getUniformLocation(program, 'u_resolution'),
 *     canvas: gl.getUniformLocation(program, 'u_canvas'),
 *   };
 *   program.uniformSetup = () => null;
 *   return program;
 * };
 */

/*
 * DEBUG: Future Use
 * pPM.setupSmaaWeights = (gl, program) => {
 *   program.attr = {
 *     position: gl.getAttribLocation(program, 'a_position'),
 *     texCoord: gl.getAttribLocation(program, 'a_texCoord'),
 *   };
 *   program.uniform = {
 *     resolution: gl.getUniformLocation(program, 'u_resolution'),
 *     canvas: gl.getUniformLocation(program, 'u_canvas'),
 *     area: gl.getUniformLocation(program, 'u_area'),
 *     search: gl.getUniformLocation(program, 'u_search'),
 *   };
 *   program.uniformSetup = () => null;
 *   program.textures = {};
 *   program.getAreaTexture = () =>
 *     // eslint-disable-next-line implicit-arrow-linebreak
 *     ''; // NO-PIG
 *   program.getSearchTexture = () =>
 *     // eslint-disable-next-line implicit-arrow-linebreak
 *     ''; // NO-PIG
 *   const areaTextureImage = new Image();
 *   areaTextureImage.src = program.getAreaTexture();
 *   areaTextureImage.onload = function () {
 *     program.textures.area = gl.createTexture();
 *     gl.bindTexture(gl.TEXTURE_2D, program.textures.area);
 *     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
 *     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); // makes clearing work
 *     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
 *     gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, areaTextureImage);
 *   };
 *   const searchTextureImage = new Image();
 *   searchTextureImage.src = program.getSearchTexture();
 *   searchTextureImage.onload = function () {
 *     program.textures.search = gl.createTexture();
 *     gl.bindTexture(gl.TEXTURE_2D, program.textures.search);
 *     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
 *     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
 *     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); // makes clearing work
 *     gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
 *     gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, searchTextureImage);
 *   };
 *   return program;
 * };
 */

/*
 * DEBUG: Future Use
 * pPM.setupGaussian = (gl, program) => {
 *   program.attr = {
 *     position: gl.getAttribLocation(program, 'a_position'),
 *     texCoord: gl.getAttribLocation(program, 'a_texCoord'),
 *   };
 *   program.uniform = {
 *     resolution: gl.getUniformLocation(program, 'u_resolution'),
 *     canvas: gl.getUniformLocation(program, 'u_canvas'),
 *     radius: gl.getUniformLocation(program, 'u_radius'),
 *     dir: gl.getUniformLocation(program, 'u_dir'),
 *   };
 *   program.uniformValues = {
 *     radius: 10,
 *     dir: {
 *       x: 1.0,
 *       y: 0.0,
 *     },
 *   };
 *   program.uniformSetup = (program) => {
 *     gl.uniform1f(program.uniform.radius, program.uniformValues.radius);
 *     gl.uniform2f(program.uniform.dir, program.uniformValues.dir.x, program.uniformValues.dir.y);
 *   };
 *   return program;
 * };
 */
